1.线程和进程的概念、并行和并发的概念
- 进程对应操作系统的运行模块,一个进程可以由多个线程具体实现。对应的是和宏观和微观上的不同。
- 线程的 5 种状态:新建、就绪、运行、堵塞、死亡
2.创建线程的方式及实现
new thread
implement runable 接口
callable :又返回值,可以抛异常。
public class Test2 { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask futureTask = new FutureTask(new callDemo()); new Thread(futureTask).start(); System.out.println(futureTask.get().toString()); } } class callDemo implements Callable { @Override public Object call() throws Exception { Thread.sleep(5000); return "call返回"; } }
通过Executor线程池进行创建
3.五种通讯方式
1.管道:速度慢,容量有限,只有父子进程能通讯
2.FIFO:任何进程间都能通讯,但速度慢
3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
4.信号量:不能传递复杂消息,只能用来同步
5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
4.CountDownLatch
堵塞主线程。手动减数。
public class CountDownLatchDemo {
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
Thread.sleep(Math.abs(new Random().nextInt(5000)));
System.out.println("第" + index + "颗龙珠已经找到");
} catch (Exception e) {
e.printStackTrace();
}
//每收集到一颗龙珠,需要等待的颗数减1
countDownLatch.countDown();
}).start();
}
//等待检查,即上述7个线程执行完毕之后,执行await后边的代码
countDownLatch.await();
System.out.println("召唤神龙");
System.out.println("我是主线程,我被阻塞了");
}
}
5.CyclicBarrier
新建一个线程。不堵塞主线程。
public class CyclicBarrierDemo {
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("7个法师召集完毕,同时出发,去往不同地方寻找龙珠!");
// summonDragon();
}
});
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
Thread.sleep(Math.abs(new Random().nextInt(3000)));
System.out.println("召集第" + index + "个法师");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
System.out.println("我是主线程,我没有被堵塞");
}
}
6.ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程
访问,其他线程则无法访问和修改。简单理解,就是每个线程变量的副本。不相互影响。使用完要手动移除掉。不然会 oom。
普通成员变量是会给多个并发线程共享,有 JMM 问题。可以使用 volatile 使得变量内存可见并且禁止指令重排序。
ThreadLocal 对象归属于没一个线程单独使用。原理是维护一个 ThreadLocalMap 进行 put、set、remove操作。其中 ThreadLocalMap 的 key 是弱引用(gc就会回收如果没有强引用指向它)。key是ThreadLocal自己处理了,但是value就需要我们自己remove。
假设:key 已经被回收了,key = null 但是 value 有个 map 的强引用指向它,所以在 GC 引用链中,它不能被回收,但是 key 不在了,value 也无法使用,value 就变成了占用内存但是又没用的数据。线程如果结束掉,整个 ThreadLocal 会给 GC 掉。只有在常驻的线程里面容易发生内存泄漏问题。
对象存放在哪里?
在Java中,栈内存归属于单个线程,每个线程都会有一个
栈内存
,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见
。
使用场景:
1.Spring 可以定义多个过滤器 Filter,拦截器 HandlerInterceptor ,这些都是带有顺序执行的。多个操作直接需要共享数据如用户登陆信息、请求响应信息等,但是每个请求直接数据又要独立互不干扰。
内存泄漏例子:
public class SimpleThreadLocal {
private static final int THREAD_LOOP_SIZE = 500;
private static final int MOCK_DIB_DATA_LOOP_SIZE = 10000;
private static ThreadLocal<List<User>> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);
for (int i = 0; i < THREAD_LOOP_SIZE; i++) {
executorService.execute(() -> {
threadLocal.set(new SimpleThreadLocal().addBigList());
Thread t = Thread.currentThread();
System.out.println(Thread.currentThread().getName());
// threadLocal.remove(); // 不取消注释的话就可能出现OOM
});
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// executorService.shutdown();
}
private List<User> addBigList() {
List<User> params = new ArrayList<>(MOCK_DIB_DATA_LOOP_SIZE);
for (int i = 0; i < MOCK_DIB_DATA_LOOP_SIZE; i++) {
params.add(new User("xuliugen", "password" + i, "男", i));
}
return params;
}
class User {
private String userName;
private String password;
private String sex;
private int age;
public User(String userName, String password, String sex, int age) {
this.userName = userName;
this.password = password;
this.sex = sex;
this.age = age;
}
}
}
7.线程池的几种实现方式
线程池就是批量先建立好一定数目的线程,使用完后在放回。减少创建销毁需要的资源。
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
// 可以自定义线程池名称
private static final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder()
.namingPattern("redisLock-schedule-pool").daemon(true).build());
线程池的工作原理
线程池刚创建时,里面没有一个线程。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a、如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b、如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
经过jmeter压力测试亲测:最大线程总数为 maximumPoolSize + 队列 数目。
8.可重用锁
- 当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁或者其他同步代码块时,如果该锁是重入锁,请求就会成功,否则阻塞。在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的。
9.什么是ABA问题,出现ABA问题JDK是如何解决的
ABA:修改了值,在修改回来。
解决:增加一个版本号,每次修改增加1
10.死锁
两个线程1、2,分别访问两个资源A、B,1线程先请求A在请求B,2线程先请求B再请求A,双方都在等待对方释放资源。造成死锁。
预防:
- 控制加锁的顺序
- 使用jdk自带工具jconsole进行死锁检测
- 使用
jstack -l
线程检测
11. join()
等待调用线程执行完毕为止。